﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Text.RegularExpressions;
using SvnLogParser.Localization;

namespace SvnLogParser
{
	public class LogParser
	{
		private const string windowsPathForbiddenChars = "<>:\"\\\\/|?*";
		private readonly Regex regSeparator;
		private readonly Regex regRevAuthorDate;
		private readonly Regex mergeInfo;
		private readonly Regex fileChangeReg;
		private readonly Regex bugTraqIssue;
		public readonly bool store_comments=true;
		public readonly string bugTraqIssuePattern = ConfigurationManager.AppSettings["bugTraqRegex"];

		private int currentLineNumber = 0;

		/// <summary>
		/// Create a new instance of LogParser
		/// </summary>
		/// <param name="store_comments">true if you want to retrieved revision comments after parsing</param>
		public LogParser(bool store_comments=false)
		{
			// Defining regexs
			RegexOptions options = RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.IgnoreCase;
			regSeparator = new Regex("^-{50,}$", options);
			// regex for line containg revNum, author and date
			// e.g. "r1 | bob@example.com | 2012-10-11 00:45:58 +0200 (jeu., 11 oct. 2012) | 1 line
			// note that author can be a name or an email ; we can have also "(no author)"
			regRevAuthorDate = new Regex(@"^r(?<revision>\d+)\s\|\s(?<author>\(?[\w.%@+-]+( [\w.%@+-]+)*\)?)\s\|\s(?<dateUTC>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}(\s\+\d+))", options);
			string filePathPattern = string.Format(@"[\\/](?:(?:[^{0}]+[\\/])*[^{0}]+(\.\w+)?)?", windowsPathForbiddenChars); // this can be a directory!
			fileChangeReg = new Regex(
				string.Format(@"^\s+(?<changeType>[ADMR])\s(?<filePath>{0})(\s\(from\s(?<fromFile>{0}):(?<fromRev>\d+)\))?$", filePathPattern),
				options
			);
			if (bugTraqIssuePattern != null) {
				bugTraqIssue = new Regex(bugTraqIssuePattern, options);
			}
			mergeInfo =  new Regex(@"^((?<reverseMerge>Reverse) )?Merged via: r(?<parentRevision>[0-9]+)(,[\s]r(?<parentRevision>[0-9]+))*$", options);
			this.store_comments = store_comments;
		}

		/// <summary>
		/// Parses a subversion log
		/// </summary>
		/// <param name="logReader"></param>
		public IEnumerable<RevisionInfo> Parse( TextReader logReader )
		{
			RevisionInfo rev;
			while ((rev = ParseOneRevision(logReader)) != null) {
				yield return rev;
			}
		}

		enum ParseStep
		{
			readRevision, readChangedsPath, readComments
		}

		public RevisionInfo ParseOneRevision(TextReader logReader)
		{
			ParseStep step = ParseStep.readRevision;
			
			RevisionInfo rev = new RevisionInfo();

			string currentLine;
			while ((currentLine = logReader.ReadLine()) != null) {
				currentLineNumber++;
				switch (step) {
					case ParseStep.readRevision:
						if (regSeparator.IsMatch(currentLine)) {
							// skip separator
						} else {						
							Match match = regRevAuthorDate.Match(currentLine);
							if (match.Success) {
								rev.revisionNum = int.Parse(match.Groups["revision"].Value);
								rev.author = match.Groups["author"].Value;
								rev.date = DateTime.Parse(match.Groups["dateUTC"].Value);
								step = ParseStep.readChangedsPath;
							} else {
								throw new Exception(string.Format(strings.ParsingErrorRevision, currentLineNumber, currentLine));
							}
						}
						break;
					case ParseStep.readChangedsPath:
						if (currentLine == "Changed paths:") {
							// skip line
						} else if (currentLine == string.Empty) {
							// ligne vide = fin de la liste des fichiers modifiés
							// prochaine étape : lecture du commentaire de livraison
							step = ParseStep.readComments;
						} else {
							// lecture liste des fichiers modifiés
							// e.g. M /ott/trunk/Interim/Assignments/listAssignments.ascx.cs
							Match m = fileChangeReg.Match(currentLine);
							if (m.Success) {
								FileChange fc = new FileChange();
								fc.ChangeType = m.Groups["changeType"].Value;
								fc.FilePath = m.Groups["filePath"].Value;
								if (m.Groups["fromFile"].Success) {
									fc.FromFile = m.Groups["fromFile"].Value;
									fc.FromRev = int.Parse(m.Groups["fromRev"].Value);
								}
								rev.filesChanged.Add(fc);
							} else {
								Match match = mergeInfo.Match(currentLine);
								if (match.Success) {
									for (int i=0; i<match.Groups["parentRevision"].Captures.Count; i++) {
										rev.parentsRevisionsNums.Add(int.Parse(match.Groups["parentRevision"].Captures[i].Value));
									}
									
									rev.reverseMerge = match.Groups["reverseMerge"].Success;
								} else {
									throw new Exception(string.Format(strings.ParsingErrorChangedFile, currentLineNumber, currentLine));
								}
							}
						}
						break;
					case ParseStep.readComments:
					    if (regSeparator.IsMatch(currentLine)) {
							// parsing révision terminé
							return rev;
						} else {
							if (bugTraqIssue != null) {
								// rechercher les tickets associés à la révision
								MatchCollection matches = bugTraqIssue.Matches(currentLine);
								if (matches.Count > 0) {
									foreach (Match match in matches) {
										rev.IssuesId.Add(int.Parse(match.Groups["issueId"].Value));
									}
								}
							}
							if (store_comments) rev.AddCommentLine(currentLine);
						}
						break;
					default:
						break;
				}
			}
			// on a atteint la fin du fichier. 
			if (step == ParseStep.readRevision && !rev.revisionNum.HasValue) {
				// ok si l'on n'a rien lu pour la révision courante
				return null;
			} else {
				throw new Exception(string.Format(strings.ParsingError, currentLineNumber));
			}
		}
	}
}
